/**s
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * MatrixFX effects library
 * http://code.google.com/p/matrix-fx/
 *
 * (c) Pete Baker 2010
 */
#include "WProgram.h"
#include "MatrixFX.h"

/**
 * Constructs a new MatrixFX object, setting the data, load and
 * clock pins of the matrix.
 */
MatrixFX::MatrixFX(int dataPin, int clockPin, int loadPin) : 
								_matrix(dataPin, clockPin, loadPin), 
								_buffer(MFX_DEFAULT_WIDTH, MFX_DEFAULT_HEIGHT) {
	this->_init();
}

/**
 * Constructs a new MatrixFX object, setting the data, load and
 * clock pins of the matrix. Also specifies the buffer dimensions.
 */
MatrixFX::MatrixFX(int dataPin, int clockPin, int loadPin, int bufferWidth, int bufferHeight) : 
								_matrix(dataPin, clockPin, loadPin), 
								_buffer(bufferWidth, bufferHeight) {
	this->_init();
}

/**
 * Initialises the FX library, setting the default effect and brightness.
 */
void MatrixFX::_init() {
	_activeEffect = rain;
	
	this->setBrightness(MFX_DEFAULT_BRIGHTNESS);
}

/**
 * Sets the LED brightness.  Must be between 0 and 15 inclusive.
 */
void MatrixFX::setBrightness(int brightness) {
	if (brightness < MFX_MIN_BRIGHTNESS || brightness > MFX_MAX_BRIGHTNESS) 
		return;
		
	_brightness = brightness;
	_matrix.setBrightness(_brightness);
}

/**
 * Initialises the given effect.
 */
void MatrixFX::initEffect(effect eff) {
	_activeEffect = eff;
	
	switch (eff) {
		case rain:
			this->effect_rain_init();
			break;
		case random_fill:
			this->effect_random_fill_init();
			break;
		case pulse:
			this->effect_pulse_init();
			break;
		case scan_vertical:
			this->effect_scan_vertical_init();
			break;
		case scan_horizontal:
			this->effect_scan_horizontal_init();
			break;
		case box_zoom:
			this->effect_box_zoom_init();
			break;
	}
}

/**
 * Initialises the next effect in the cycle.
 */
void MatrixFX::nextEffect() {	
	switch (_activeEffect) {
		case rain: 
			initEffect(random_fill);
			break;
		case random_fill:
			initEffect(pulse);
			break;
		case pulse:
			initEffect(scan_vertical);
			break;
		case scan_vertical:
			initEffect(scan_horizontal);
			break;
		case scan_horizontal: 
			initEffect(box_zoom);
			break;
		case box_zoom:
			initEffect(rain);
			break;
	}
}

/**
 * Updates the current effect, drawing the next frame.
 *
 * This uses a function pointer to point to the relevant
 * _effect_update() method.
 */
void MatrixFX::update() {
	(this->*_update)();
}

/**
 * Clears the matrix, turning off all LEDs.
 */
void MatrixFX::clear() {
	_matrix.clear();
}

/**
 * Returns the total number of LEDs in the matrix.
 */
int MatrixFX::getBufferSize() {
	return _buffer.height() * _buffer.width();
}

/**
 * Increases the LED brightness.
 */
void MatrixFX::increaseBrightness() {
	if (_brightness < MFX_MAX_BRIGHTNESS) _brightness++;
	this->setBrightness(_brightness);
}

/**
 * Decreases the LED brightness.
 */
void MatrixFX::decreaseBrightness() {
	if (_brightness > MFX_MIN_BRIGHTNESS) _brightness--;
	this->setBrightness(_brightness);
}

/**
 * Effect: Rain.  
 * Init method.
 *
 * Draws like rain on the LED matrix.
 */
void MatrixFX::effect_rain_init() {
	_fx.x = 0;	//initialise x
  
	fill(0);
	_update = &MatrixFX::effect_rain_update;	//set the update method
}

/**
 * Update method for the rain effect.
 */
void MatrixFX::effect_rain_update() {
   	emptyLine(_fx.x);									  		//empty the current line
   	_buffer.write(_fx.x, random(0, _buffer.height() - 1), 1); 	//write a single random pixel on the current line.
    this->printSpriteVertical(_fx.x);        					//print sprite with vertical offset
  
    _fx.x = (_fx.x == 0) ? _buffer.height() - 1: _fx.x - 1;  	//decrement x or reset if zero.
}

/**
 * Effect: Random Fill. 
 * Init method.
 *
 * Turns each LED on in a random order, then repeats
 * turning each LED off in a random order.
 */
void MatrixFX::effect_random_fill_init() {
	_fx.x = 0;		//init all the state vars.
	_fx.y = 0;
	_fx.inc = 0;
  	_fx.state = 0;  

	fill(_fx.state);	//reset the buffer
	_update = &MatrixFX::effect_random_fill_update;	//set the update method
}

/**
 * Update method for the random_fill effect.
 */
void MatrixFX::effect_random_fill_update() {
  	_fx.x = rand() % _buffer.width();	//select  
  	_fx.y = rand() % _buffer.height();	//random pixel

  	if (_buffer.read(_fx.x, _fx.y) == _fx.state) {
    	_buffer.write(_fx.x, _fx.y, (_fx.state == 0) ? 1 : 0);
   		_matrix.write(0, 0, _buffer);
        		
   		_fx.inc++;
  	}

    if (_fx.inc == this->getBufferSize()) { //if all the pixels are lit
		_fx.state = _fx.state ^ 1;			//flip the state
	}
}

/**
 * Effect: Pulse
 * Init method.
 * 
 * Pulses all the LEDs on and off between zero brightness
 * and the user-set brightness
 */
void MatrixFX::effect_pulse_init() {
	_fx.x = 0;	//brightness
	_fx.inc = 1;	//incrementor
    
    this->setBrightness(_fx.x);
    this->fill(1);
    _matrix.write(0, 0, _buffer);

	_update = &MatrixFX::effect_pulse_update;
}

/**
 * Update method for the pulse effect.
 */
void MatrixFX::effect_pulse_update() { 
	_fx.x += _fx.inc;
        
	this->setBrightness(_fx.x);
        
	//limit the brightness between 0 and the user-set brightness
    if (_fx.x == 0 || _fx.x == _brightness) {
        _fx.inc *= -1;
    }
}

/**
 * Effect: Scan | Vertical
 * Init method.
 * 
 * Draws a horizontal line that scans vertically up and down
 * the matrix.
 */
void MatrixFX::effect_scan_vertical_init() {
	this->effect_scan_init(false);
}

/**
 * Effect: Scan | Horizontal
 * Init method.
 *
 * Draws a horizontal line that scans vertically up and down
 * the matrix.
 */
void MatrixFX::effect_scan_horizontal_init() {
	this->effect_scan_init(true);
}

/**
 * Common Init method for both the scan effects.
 */
void MatrixFX::effect_scan_init(bool isHorizontal) {
	_fx.x = 0;		//line index
	_fx.inc = 1;	//incrementor
	_fx.isHorizontal = isHorizontal; //horizontal or vertical
	
	fill(0);
	
	//write the first column of the buffer
	for (int i = 0; i < _buffer.width(); i++) { 
		_fx.isHorizontal ? _buffer.write(i, 0, 1) : _buffer.write(1, i, 1);
	}
	
	_update = &MatrixFX::effect_scan_update;	//set the update method
}

/**
 * Update method for the scan effect.
 */
void MatrixFX::effect_scan_update() {
	_fx.isHorizontal ? printSpriteVertical(_fx.x) : printSpriteHorizontal(_fx.x);

	_fx.x += _fx.inc;	//increment or decrement line
	if (_fx.x == 0 || 
		(_fx.isHorizontal && (_fx.x == _buffer.width() - 1) || 
		(!_fx.isHorizontal && (_fx.x == _buffer.height() - 1)))) {
		_fx.inc *= -1;
	}		
} 

/**
 * Effect: Box zoom
 * Init method.
 * 
 * Draws a box onto the matrix then zooms in on it in a cycle.
 */
void MatrixFX::effect_box_zoom_init() {
	_fx.x = (_buffer.width() / 2);		//start x position
	_fx.y = (_buffer.height() / 2);		//start y position
	_fx.inc = 2;							//box side length
	
	_update = &MatrixFX::effect_box_zoom_update;	//set the update method
}

/**
 * Update method for the box zoom effect.
 */
void MatrixFX::effect_box_zoom_update() {
	_matrix.clear();		//reset buffer.
	
	//draw the box sides
	for (int i = 0; i < _fx.inc; i++) {
		_matrix.write(_fx.x + i, _fx.y, 1);
		_matrix.write(_fx.x, _fx.y + i, 1);
		_matrix.write(_fx.x + _fx.inc - 1, _fx.y + i, 1);
		_matrix.write(_fx.x + i, _fx.y + _fx.inc - 1, 1);
	}
		
	_fx.inc += 2;	//increment box size by 2;
	_fx.x--;		//move the box start position 
	_fx.y--;		//closer to the corner
	
	//if the box is in the corner reset
	if (_fx.x < 0 || _fx.y < 0) {
		effect_box_zoom_init();
	}
}

/**
 * Empties the specified line of the sprite.
 */
void MatrixFX::emptyLine(int x) {
   for (int i = 0; i < _buffer.width(); i++) {
      _buffer.write(x, i, 0);      // sets the sprite pixel located at i, j to 0
   }
}

/**
 * Prints the sprite with a horizontal offset.
 */
void MatrixFX::printSpriteHorizontal(int xPos) {
    _matrix.write(xPos, 0, _buffer);     // place sprite on screen
    _matrix.write(xPos - _buffer.width(), 0, _buffer); // fill the rest of the screen with the remainder of the sprite
}

/**
 * Prints the sprite with a vertical offset
 */
void MatrixFX::printSpriteVertical(int yPos) {
    _matrix.write(0, yPos, _buffer);     // place sprite on screen
    _matrix.write(0, yPos - _buffer.height(), _buffer); // fill the rest of the screen with the remainder of the sprite
}

/**
 * Fills the buffer value with either all 0 or all 1.
 */
void MatrixFX::fill(byte state) {
  for (int i = 0; i < _buffer.width(); i++) {
    for (int j = 0; j < _buffer.height(); j++) {
      _buffer.write(i, j, state);
    }
  }
}